/*
Copyright Digital Asset Holdings, LLC All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package channel

import (
	"bytes"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"regexp"
	"sync"
	"testing"
	"time"

	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric/core/config/configtest"
	"github.com/hyperledger/fabric/internal/peer/common"
	"github.com/hyperledger/fabric/internal/peer/common/mock"
	"github.com/hyperledger/fabric/internal/pkg/identity"
	msptesttools "github.com/hyperledger/fabric/msp/mgmt/testtools"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
)

//go:generate counterfeiter -o mock/signer_serializer.go --fake-name SignerSerializer . signerSerializer

type signerSerializer interface {
	identity.SignerSerializer
}

type timeoutOrderer struct {
	counter int
	net.Listener
	*grpc.Server
	nextExpectedSeek uint64
	t                *testing.T
	blockChannel     chan uint64
}

func newOrderer(port int, t *testing.T) *timeoutOrderer {
	srv := grpc.NewServer()
	lsnr, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
	if err != nil {
		panic(err)
	}
	o := &timeoutOrderer{
		Server:           srv,
		Listener:         lsnr,
		t:                t,
		nextExpectedSeek: uint64(1),
		blockChannel:     make(chan uint64, 1),
		counter:          1,
	}
	orderer.RegisterAtomicBroadcastServer(srv, o)
	go srv.Serve(lsnr)
	return o
}

func (o *timeoutOrderer) Shutdown() {
	o.Server.Stop()
	o.Listener.Close()
}

func (*timeoutOrderer) Broadcast(orderer.AtomicBroadcast_BroadcastServer) error {
	panic("Should not have been called")
}

func (o *timeoutOrderer) SendBlock(seq uint64) {
	o.blockChannel <- seq
}

func (o *timeoutOrderer) Deliver(stream orderer.AtomicBroadcast_DeliverServer) error {
	o.timeoutIncrement()
	if o.counter > 5 {
		o.sendBlock(stream, 0)
	}
	return nil
}

func (o *timeoutOrderer) sendBlock(stream orderer.AtomicBroadcast_DeliverServer, seq uint64) {
	block := &cb.Block{
		Header: &cb.BlockHeader{
			Number: seq,
		},
	}
	stream.Send(&orderer.DeliverResponse{
		Type: &orderer.DeliverResponse_Block{Block: block},
	})
}

func (o *timeoutOrderer) timeoutIncrement() {
	o.counter++
}

var once sync.Once

// / mock deliver client for UT
type mockDeliverClient struct {
	err error
}

func (m *mockDeliverClient) readBlock() (*cb.Block, error) {
	if m.err != nil {
		return nil, m.err
	}
	return &cb.Block{}, nil
}

func (m *mockDeliverClient) GetSpecifiedBlock(num uint64) (*cb.Block, error) {
	return m.readBlock()
}

func (m *mockDeliverClient) GetOldestBlock() (*cb.Block, error) {
	return m.readBlock()
}

func (m *mockDeliverClient) GetNewestBlock() (*cb.Block, error) {
	return m.readBlock()
}

func (m *mockDeliverClient) Close() error {
	return nil
}

// InitMSP init MSP
func InitMSP() {
	once.Do(initMSP)
}

func initMSP() {
	err := msptesttools.LoadMSPSetupForTesting()
	if err != nil {
		panic(fmt.Errorf("Fatal error when reading MSP config: err %s", err))
	}
}

func mockBroadcastClientFactory() (common.BroadcastClient, error) {
	return common.GetMockBroadcastClient(nil), nil
}

func TestCreateChain(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	defer os.Remove(mockchain + ".block")

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)

	AddFlags(cmd)

	args := []string{"-c", mockchain, "-o", "localhost:7050"}
	cmd.SetArgs(args)

	if err := cmd.Execute(); err != nil {
		t.Fail()
		t.Errorf("expected create command to succeed")
	}

	filename := mockchain + ".block"
	if _, err := os.Stat(filename); err != nil {
		t.Fail()
		t.Errorf("expected %s to exist", filename)
	}
}

func TestCreateChainWithOutputBlock(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)
	AddFlags(cmd)

	tempDir := t.TempDir()

	outputBlockPath := filepath.Join(tempDir, "output.block")
	args := []string{"-c", mockchain, "-o", "localhost:7050", "--outputBlock", outputBlockPath}
	cmd.SetArgs(args)
	defer func() { outputBlock = "" }()

	err = cmd.Execute()
	require.NoError(t, err, "execute should succeed")

	_, err = os.Stat(outputBlockPath)
	require.NoErrorf(t, err, "expected %s to exist", outputBlockPath)
}

func TestCreateChainWithDefaultAnchorPeers(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	defer os.Remove(mockchain + ".block")

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)
	AddFlags(cmd)
	args := []string{"-c", mockchain, "-o", "localhost:7050"}
	cmd.SetArgs(args)

	if err := cmd.Execute(); err != nil {
		t.Fail()
		t.Errorf("expected create command to succeed")
	}
}

func TestCreateChainWithWaitSuccess(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{err: nil},
	}
	fakeOrderer := newOrderer(8101, t)
	defer fakeOrderer.Shutdown()

	cmd := createCmd(mockCF)
	AddFlags(cmd)
	args := []string{"-c", mockchain, "-o", "localhost:8101", "-t", "10s"}
	cmd.SetArgs(args)

	if err := cmd.Execute(); err != nil {
		t.Fail()
		t.Errorf("expected create command to succeed")
	}
}

func TestCreateChainWithTimeoutErr(t *testing.T) {
	defer viper.Reset()
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{err: errors.New("bobsled")},
	}
	fakeOrderer := newOrderer(8102, t)
	defer fakeOrderer.Shutdown()

	// failure - connects to orderer but times out waiting for channel to
	// be created
	cmd := createCmd(mockCF)
	AddFlags(cmd)
	channelCmd.AddCommand(cmd)
	args := []string{"create", "-c", mockchain, "-o", "localhost:8102", "-t", "10ms", "--connTimeout", "1s"}
	channelCmd.SetArgs(args)

	if err := channelCmd.Execute(); err == nil {
		t.Error("expected create chain to fail with deliver error")
	} else {
		require.Contains(t, err.Error(), "timeout waiting for channel creation")
	}

	// failure - point to bad port and time out connecting to orderer
	args = []string{"create", "-c", mockchain, "-o", "localhost:0", "-t", "1s", "--connTimeout", "10ms"}
	channelCmd.SetArgs(args)

	if err := channelCmd.Execute(); err == nil {
		t.Error("expected create chain to fail with deliver error")
	} else {
		require.Contains(t, err.Error(), "failed connecting")
	}
}

func TestCreateChainBCFail(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	defer os.Remove(mockchain + ".block")

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	sendErr := errors.New("luge")
	mockCF := &ChannelCmdFactory{
		BroadcastFactory: func() (common.BroadcastClient, error) {
			return common.GetMockBroadcastClient(sendErr), nil
		},
		Signer:        signer,
		DeliverClient: &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)
	AddFlags(cmd)
	args := []string{"-c", mockchain, "-o", "localhost:7050"}
	cmd.SetArgs(args)

	expectedErrMsg := sendErr.Error()
	if err := cmd.Execute(); err == nil {
		t.Error("expected create chain to fail with broadcast error")
	} else {
		if err.Error() != expectedErrMsg {
			t.Errorf("Run create chain get unexpected error: %s(expected %s)", err.Error(), expectedErrMsg)
		}
	}
}

func TestCreateChainDeliverFail(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchain := "mockchain"

	defer os.Remove(mockchain + ".block")

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	sendErr := fmt.Errorf("skeleton")
	mockCF := &ChannelCmdFactory{
		BroadcastFactory: func() (common.BroadcastClient, error) {
			return common.GetMockBroadcastClient(sendErr), nil
		},
		Signer:        signer,
		DeliverClient: &mockDeliverClient{err: sendErr},
	}
	cmd := createCmd(mockCF)
	AddFlags(cmd)
	args := []string{"-c", mockchain, "-o", "localhost:7050"}
	cmd.SetArgs(args)

	expectedErrMsg := sendErr.Error()
	if err := cmd.Execute(); err == nil {
		t.Errorf("expected create chain to fail with deliver error")
	} else {
		if err.Error() != expectedErrMsg {
			t.Errorf("Run create chain get unexpected error: %s(expected %s)", err.Error(), expectedErrMsg)
		}
	}
}

func createTxFile(filename string, typ cb.HeaderType, channelID string) (*cb.Envelope, error) {
	ch := &cb.ChannelHeader{Type: int32(typ), ChannelId: channelID}
	data, err := proto.Marshal(ch)
	if err != nil {
		return nil, err
	}

	p := &cb.Payload{Header: &cb.Header{ChannelHeader: data}}
	data, err = proto.Marshal(p)
	if err != nil {
		return nil, err
	}

	env := &cb.Envelope{Payload: data}
	data, err = proto.Marshal(env)
	if err != nil {
		return nil, err
	}

	if err = os.WriteFile(filename, data, 0o644); err != nil {
		return nil, err
	}

	return env, nil
}

func TestCreateChainFromTx(t *testing.T) {
	defer resetFlags()
	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchannel := "mockchannel"
	dir := t.TempDir()

	// this could be created by the create command
	defer os.Remove(mockchannel + ".block")

	file := filepath.Join(dir, mockchannel)

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}
	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)
	AddFlags(cmd)

	// Error case 0
	args := []string{"-c", "", "-f", file, "-o", "localhost:7050"}
	cmd.SetArgs(args)
	err = cmd.Execute()
	require.Error(t, err, "Create command should have failed because channel ID is not specified")
	require.Contains(t, err.Error(), "must supply channel ID")

	// Error case 1
	args = []string{"-c", mockchannel, "-f", file, "-o", "localhost:7050"}
	cmd.SetArgs(args)
	err = cmd.Execute()
	require.Error(t, err, "Create command should have failed because tx file does not exist")
	msgExpr := regexp.MustCompile(`channel create configuration tx file not found.*no such file or directory`)
	require.True(t, msgExpr.MatchString(err.Error()))

	// Success case: -f option is empty
	args = []string{"-c", mockchannel, "-f", "", "-o", "localhost:7050"}
	cmd.SetArgs(args)
	err = cmd.Execute()
	require.NoError(t, err)

	// Success case
	args = []string{"-c", mockchannel, "-f", file, "-o", "localhost:7050"}
	cmd.SetArgs(args)
	_, err = createTxFile(file, cb.HeaderType_CONFIG_UPDATE, mockchannel)
	require.NoError(t, err, "Couldn't create tx file")
	err = cmd.Execute()
	require.NoError(t, err)
}

func TestCreateChainInvalidTx(t *testing.T) {
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchannel := "mockchannel"

	dir := t.TempDir()

	// this is created by create command
	defer os.Remove(mockchannel + ".block")

	file := filepath.Join(dir, mockchannel)

	signer, err := common.GetDefaultSigner()
	if err != nil {
		t.Fatalf("Get default signer error: %v", err)
	}

	mockCF := &ChannelCmdFactory{
		BroadcastFactory: mockBroadcastClientFactory,
		Signer:           signer,
		DeliverClient:    &mockDeliverClient{},
	}

	cmd := createCmd(mockCF)

	AddFlags(cmd)

	args := []string{"-c", mockchannel, "-f", file, "-o", "localhost:7050"}
	cmd.SetArgs(args)

	// bad type CONFIG
	if _, err = createTxFile(file, cb.HeaderType_CONFIG, mockchannel); err != nil {
		t.Fatalf("couldn't create tx file")
	}

	defer os.Remove(file)

	if err = cmd.Execute(); err == nil {
		t.Errorf("expected error")
	} else if _, ok := err.(InvalidCreateTx); !ok {
		t.Errorf("invalid error")
	}

	// bad channel name - does not match one specified in command
	if _, err = createTxFile(file, cb.HeaderType_CONFIG_UPDATE, "different_channel"); err != nil {
		t.Fatalf("couldn't create tx file")
	}

	if err = cmd.Execute(); err == nil {
		t.Errorf("expected error")
	} else if _, ok := err.(InvalidCreateTx); !ok {
		t.Errorf("invalid error")
	}

	// empty channel
	if _, err = createTxFile(file, cb.HeaderType_CONFIG_UPDATE, ""); err != nil {
		t.Fatalf("couldn't create tx file")
	}

	if err := cmd.Execute(); err == nil {
		t.Errorf("expected error")
	} else if _, ok := err.(InvalidCreateTx); !ok {
		t.Errorf("invalid error")
	}
}

func TestCreateChainNilCF(t *testing.T) {
	defer viper.Reset()
	defer resetFlags()

	InitMSP()
	configtest.SetDevFabricConfigPath(t)

	mockchannel := "mockchannel"
	dir := t.TempDir()

	// this is created by create command
	defer os.Remove(mockchannel + ".block")
	file := filepath.Join(dir, mockchannel)

	// Error case: grpc error
	viper.Set("orderer.client.connTimeout", 10*time.Millisecond)
	cmd := createCmd(nil)
	AddFlags(cmd)
	args := []string{"-c", mockchannel, "-f", file, "-o", "localhost:7050"}
	cmd.SetArgs(args)
	err := cmd.Execute()
	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to create deliver client")

	// Error case: invalid ordering service endpoint
	args = []string{"-c", mockchannel, "-f", file, "-o", "localhost"}
	cmd.SetArgs(args)
	err = cmd.Execute()
	require.Error(t, err)
	require.Contains(t, err.Error(), "ordering service endpoint localhost is not valid or missing")

	// Error case: invalid ca file
	defer os.RemoveAll(dir) // clean up
	channelCmd.AddCommand(cmd)
	args = []string{"create", "-c", mockchannel, "-f", file, "-o", "localhost:7050", "--tls", "true", "--cafile", dir + "/ca.pem"}
	channelCmd.SetArgs(args)
	err = channelCmd.Execute()
	require.Error(t, err)
	t.Log(err)
	require.Contains(t, err.Error(), "no such file or directory")
}

func TestSanityCheckAndSignChannelCreateTx(t *testing.T) {
	defer resetFlags()

	signer := &mock.SignerSerializer{}
	env := &cb.Envelope{}
	env.Payload = make([]byte, 10)
	var err error

	// Error case 1
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.Error(t, err, "Error expected for nil payload")
	require.Contains(t, err.Error(), "bad payload")

	// Error case 2
	p := &cb.Payload{Header: nil}
	data, err1 := proto.Marshal(p)
	require.NoError(t, err1)
	env = &cb.Envelope{Payload: data}
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.Error(t, err, "Error expected for bad payload header")
	require.Contains(t, err.Error(), "bad header")

	// Error case 3
	bites := bytes.NewBufferString("foo").Bytes()
	p = &cb.Payload{Header: &cb.Header{ChannelHeader: bites}}
	data, err = proto.Marshal(p)
	require.NoError(t, err)
	env = &cb.Envelope{Payload: data}
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.Error(t, err, "Error expected for bad channel header")
	require.Contains(t, err.Error(), "could not unmarshall channel header")

	// Error case 4
	mockchannel := "mockchannel"
	cid := channelID
	channelID = mockchannel
	defer func() {
		channelID = cid
	}()
	ch := &cb.ChannelHeader{Type: int32(cb.HeaderType_CONFIG_UPDATE), ChannelId: mockchannel}
	data, err = proto.Marshal(ch)
	require.NoError(t, err)
	p = &cb.Payload{Header: &cb.Header{ChannelHeader: data}, Data: bytes.NewBufferString("foo").Bytes()}
	data, err = proto.Marshal(p)
	require.NoError(t, err)
	env = &cb.Envelope{Payload: data}
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.Error(t, err, "Error expected for bad payload data")
	require.Contains(t, err.Error(), "Bad config update env")

	// Error case 5
	signer = &mock.SignerSerializer{}
	signer.SerializeReturns(nil, errors.New("bad signer header"))
	env, err = protoutil.CreateSignedEnvelope(
		cb.HeaderType_CONFIG_UPDATE,
		"mockchannel",
		nil,
		&cb.ConfigEnvelope{},
		0,
		0)
	require.NoError(t, err)
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.EqualError(t, err, "bad signer header")

	// Error case 6
	signer = &mock.SignerSerializer{}
	signer.SignReturns(nil, errors.New("signer failed to sign"))
	env, err = protoutil.CreateSignedEnvelope(
		cb.HeaderType_CONFIG_UPDATE,
		"mockchannel",
		nil,
		&cb.ConfigEnvelope{},
		0,
		0)
	require.NoError(t, err)
	_, err = sanityCheckAndSignConfigTx(env, signer)
	require.EqualError(t, err, "signer failed to sign")
}
